import errors from 'errno'
import 'setimmediate'
import normalize from './normalize'
import MemoryFsError from './memoryFsError'
import { FILE_TYPE as TYPE } from '../type'

const isDir = item => {
  return item && item.type === TYPE.DIR
}

const isFile = item => {
  return item && item.type === TYPE.FILE
}

const pathToArray = path => {
  path = normalize(path)
  const nix = /^\//.test(path)
  if (!nix) {
    if (!/^[A-Za-z]:/.test(path)) {
      throw new MemoryFsError(errors.code.EINVAL, path)
    }
    path = path.replace(/[\\/]+/g, '\\')
    path = path.split(/[\\/]/)
    path[0] = path[0].toUpperCase()
  } else {
    path = path.replace(/\/+/g, '/')
    path = path.substr(1).split('/')
  }
  if (!path[path.length - 1]) path.pop()
  return path
}

const trueFn = () => true
const falseFn = () => false

let uid = 0

class FileNode {
  constructor (type, name, privateObj) {
    this.id = uid++
    this.type = type
    this.name = name
    this.children = type === TYPE.DIR ? [] : null
    this.parent = null
    this.private = privateObj || {}
  }
  appendChild (child) {
    child.parent = this
    this.children.push(child)
  }
  removeChild (child) {
    let index = this.children.indexOf(child)
    child.parent = null
    this.children.splice(index, 1)
  }
  getChild (name) {
    const result = this.children.filter(item => item.name === name)
    return result.length && result[0]
  }
  toJSON () {
    return {
      type: this.type,
      name: this.name,
      children: this.children,
      private: {
        content: this.private.content
      }
    }
  }
}

export default class MemoryFs {
  constructor (baseUrl) {
    this.baseUrl = baseUrl || '/'
    this.root = new FileNode(TYPE.DIR, this.baseUrl)
  }

  meta (_path) {
    const path = pathToArray(_path)
    if (path.length === 0) {
      return this.root
    }
    let current = this.root
    let i = 0
    for (;i < path.length - 1; i++) {
      if (!isDir(current.getChild(path[i]))) return
      current = current.getChild(path[i])
    }
    return current.getChild(path[i])
  }

  statSync (_path) {
    let current = this.meta(_path)
    if (isDir(current)) {
      return {
        isFile: falseFn,
        isDirectory: trueFn
      }
    } else if (isFile(current)) {
      return {
        isFile: trueFn,
        isDir: falseFn
      }
    } else {
      throw new MemoryFsError(errors.code.ENOENT, _path, 'stat')
    }
  }

  existsSync (_path) {
    return !!this.meta(_path)
  }

  mkdirpSync (_path) {
    if (this.existsSync(_path)) {
      throw new MemoryFsError(errors.code.EEXIST, _path, 'mkdirp')
    }
    const path = pathToArray(_path)
    if (path.length === 0) return
    let current = this.root
    for (let i = 0; i < path.length; i++) {
      if (isFile(current.getChild(path[i]))) throw new MemoryFsError(errors.code.ENOTDIR, _path, 'mkdirp')
      else if (!isDir(current.getChild(path[i]))) {
        const dir = new FileNode(TYPE.DIR, path[i], {
          lastModifyTime: new Date().getTime()
        })
        current.appendChild(dir)
      }
      current = current.getChild(path[i])
    }
  }

  mkdirSync (_path) {
    if (this.existsSync(_path)) {
      throw new MemoryFsError(errors.code.EEXIST, _path, 'mkdir')
    }
    const path = pathToArray(_path)
    if (path.length === 0) return
    let current = this.root
    let i = 0
    for (; i < path.length - 1; i++) {
      current = current.getChild(path[i])
      if (!isDir(current)) {
        throw new MemoryFsError(errors.code.ENOENT, _path, 'mkdir')
      }
    }
    if (isDir(current.getChild(path[i]))) {
      throw new MemoryFsError(errors.code.EEXIST, _path, 'mkdir')
    } else if (isFile(current.getChild(path[i]))) {
      throw new MemoryFsError(errors.code.ENOTDIR, _path, 'mkdir')
    }
    const dir = new FileNode(TYPE.DIR, path[i], {
      lastModifyTime: new Date().getTime()
    })
    current.appendChild(dir)
  }

  writeFileSync (_path, content) {
    if (this.existsSync(_path)) {
      throw new MemoryFsError(errors.code.EEXIST, _path, 'readFile')
    }
    if (content === undefined || content === null) {
      throw new MemoryFsError(errors.code.ENOTEMPTY, _path, 'writeFile')
    }
    const path = pathToArray(_path)
    if (path.length === 0) throw new MemoryFsError(errors.code.EISDIR, _path, 'writeFile')
    let current = this.root
    let i = 0
    for (; i < path.length - 1; i++) {
      if (isFile(current.getChild(path[i]))) {
        throw new MemoryFsError(errors.code.ENOENT, _path, 'writeFile')
      } else if (!isDir(current.getChild(path[i]))) {
        const dir = new FileNode(TYPE.DIR, path[i], {
          lastModifyTime: new Date().getTime()
        })
        current.appendChild(dir)
      }
      current = current.getChild(path[i])
    }
    if (isDir(current.getChild(path[i]))) {
      throw new MemoryFsError(errors.code.EISDIR, _path, 'writeFile')
    } else if (isFile(current.getChild)) {
      throw new MemoryFsError(errors.code.EEXIST, _path, 'writeFile')
    }
    const file = new FileNode(TYPE.FILE, path[i], {
      content,
      initContent: content,
      lastModifyTime: new Date().getTime()
    })
    current.appendChild(file)
  }

  readFileSync (_path) {
    if (!this.existsSync(_path)) {
      throw new MemoryFsError(errors.code.ENOENT, _path, 'readFile')
    }
    const path = pathToArray(_path)
    let current = this.root
    let i = 0
    for (; i < path.length - 1; i++) {
      if (!isDir(current.getChild(path[i]))) {
        throw new MemoryFsError(errors.code.ENOENT, _path, 'readFile')
      }
      current = current.getChild(path[i])
    }
    current = current.getChild(path[i])
    if (!isFile(current)) {
      if (isDir(current)) {
        throw new MemoryFsError(errors.code.EISDIR, _path, 'readFile')
      } else {
        throw new MemoryFsError(errors.code.ENOENT, _path, 'readFile')
      }
    }
    return current
  }

  readdirSync (_path) {
    const path = pathToArray(_path)
    if (path.length === 0) {
      return this.root.children.map(item => item.name)
    }
    let current = this.root
    let i = 0
    for (; i < path.length - 1; i++) {
      if (!isDir(current.getChild(path[i]))) {
        throw new MemoryFsError(errors.code.ENOENT, _path, 'readdir')
      }
      current = current.getChild(path[i])
    }
    current = current.getChild(path[i])
    if (!isDir(current)) {
      if (isFile(current)) {
        throw new MemoryFsError(errors.code.ENOTDIR, _path, 'readdir')
      } else {
        throw new MemoryFsError(errors.code.ENOENT, _path, 'readdir')
      }
    }
    return current.children.map(item => item.name)
  }

  _remove (_path, type, fn) {
    const path = pathToArray(_path)
    const operation = type === TYPE.FILE ? 'unlink' : 'rmdir'
    if (path.length === 0) {
      throw new MemoryFsError(errors.code.EPERM, _path, operation)
    }
    let current = this.root
    let i = 0
    for (; i < path.length - 1; i++) {
      if (!isDir(current.getChild(path[i]))) {
        throw new MemoryFsError(errors.code.ENOENT, _path, operation)
      }
      current = current.getChild(path[i])
    }
    current = current.getChild(path[i])
    if (!fn(current)) {
      throw new MemoryFsError(errors.code.ENOENT, _path, operation)
    }
    current.parent.removeChild(current)
    this._destroy(current)
  }

  _destroy (node) {
    if (!node || !node.children) return
    for (let i = 0; i < node.children.length; i++) {
      this._destroy(node.children[i])
      node.removeChild(node.children[i])
    }
  }

  rmdirSync (_path) {
    return this._remove(_path, TYPE.DIR, isDir)
  }

  unlinkSync (_path) {
    return this._remove(_path, TYPE.FILE, isFile)
  }

  renameSync (_path, name) {
    if (!name) {
      throw new MemoryFsError(errors.code.EINVAL, _path, 'rename')
    }
    const path = pathToArray(_path)
    if (path.length === 0) {
      throw new MemoryFsError(errors.code.EPERM, _path, 'rename')
    }
    let current = this.root
    let i = 0
    for (; i < path.length - 1; i++) {
      if (!isDir(current.getChild(path[i]))) {
        throw new MemoryFsError(errors.code.ENOENT, _path, 'rename')
      }
      current = current.getChild(path[i])
    }
    current = current.getChild(path[i])
    if (!isFile(current) && !isDir(current)) {
      throw new MemoryFsError(errors.code.ENOENT, _path, 'rename')
    }
    current.name = name
    current.lastModifyTime = new Date().getTime()
  }

  tempSaveFileSync (_path, content) {
    let current = this.readFileSync(_path)
    current.private.content = content
  }

  saveFileSync (_path) {
    let current = this.readFileSync(_path)
    current.private.initContent = current.private.content
  }

  exists (_path, callback) {
    return callback(this.existsSync(_path))
  }

  writeFile (_path, content, callback) {
    try {
      this.writeFileSync(_path, content)
    } catch (e) {
      return callback(e)
    }
    return callback()
  }

  rename (_path, name, callback) {
    try {
      this.renameSync(_path, name)
    } catch (e) {
      return callback(e)
    }
    return callback()
  }
}

['stat', 'readdir', 'mkdirp', 'rmdir', 'unlink', 'mkdir', 'readFile'].forEach(function (fn) {
  MemoryFs.prototype[fn] = function (path, callback) {
    let result
    try {
      result = this[fn + 'Sync'](path)
    } catch (e) {
      setImmediate(function () {
        callback(e)
      })
      return
    }
    setImmediate(function () {
      callback(null, result)
    })
  }
})
